package top.hcode.hoj.remoteJudge.task.Impl;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Validator;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.URLDecoder;
import cn.hutool.core.text.UnicodeUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.nutz.http.*;
import org.nutz.json.Json;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import top.hcode.hoj.exception.ServiceException;
import top.hcode.hoj.pojo.entity.judge.JudgeCase;
import top.hcode.hoj.remoteJudge.entity.RemoteJudgeDTO;
import top.hcode.hoj.remoteJudge.entity.RemoteJudgeRes;
import top.hcode.hoj.remoteJudge.entity.RemoteOjLoginDTO;
import top.hcode.hoj.remoteJudge.task.RemoteJudgeStrategy;
import top.hcode.hoj.utils.Constants;
import top.hcode.hoj.utils.NutzHttpClient;
import top.hcode.hoj.utils.UserAgentFakeUtils;

import java.net.HttpURLConnection;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author orangej
 * @since 2024/12/11
 */
@Slf4j
public class LuoguJudge extends RemoteJudgeStrategy {

    private static final String HOST = "https://www.luogu.com.cn";

    // https://docs.lgapi.cn/open/openapi#tag/评测/paths/~1judge~1result/get
    /*
0, Waiting, 任务等待执行
1, Judging, 评测中
2, Compile Error, 编译失败
3, Output Limit Exceeded, 输出超限
4, Memory Limit Exceeded, 内存超限
5, Time Limit Exceeded, 运行时间超限
6, Wrong Answer, 答案错误
7, Runtime Error, 运行时错误
11, Invalid, 结果非法（一般是内部错误等，可以反馈）
12, Accepted, 结果正确、评测通过
14, Overall Unaccepted, 评测不通过（评测结果、子任务中使用，根据计分方式返回的评测失败）
    * */
    private static final Map<Integer, Constants.Judge> LUOGU_STATUS_MAP =
            MapUtil.builder(new HashMap<Integer, Constants.Judge>())
                    .put(0, Constants.Judge.STATUS_JUDGING)
                    .put(1, Constants.Judge.STATUS_JUDGING)
                    .put(2, Constants.Judge.STATUS_COMPILE_ERROR)
                    .put(3, Constants.Judge.STATUS_RUNTIME_ERROR)
                    .put(4, Constants.Judge.STATUS_MEMORY_LIMIT_EXCEEDED)
                    .put(5, Constants.Judge.STATUS_TIME_LIMIT_EXCEEDED)
                    .put(6, Constants.Judge.STATUS_WRONG_ANSWER)
                    .put(7, Constants.Judge.STATUS_RUNTIME_ERROR)
                    .put(11, Constants.Judge.STATUS_SYSTEM_ERROR)
                    .put(12, Constants.Judge.STATUS_ACCEPTED)
                    .put(14, Constants.Judge.STATUS_PARTIAL_ACCEPTED)
                    .map();

    private NutzHttpClient httpClient;
    private CsrfTokenInterceptor csrfTokenInterceptor;

    public LuoguJudge() {
        httpClient = new NutzHttpClient();
        httpClient.setUserAgent(UserAgentFakeUtils.getRandomUserAgent());

        csrfTokenInterceptor = new CsrfTokenInterceptor();
        httpClient.addInterceptor(csrfTokenInterceptor);
    }

    @Override
    public void submit() {
        RemoteJudgeDTO remoteJudgeDTO = getRemoteJudgeDTO();
        if (remoteJudgeDTO.getCompleteProblemId() == null || remoteJudgeDTO.getUserCode() == null) {
            return;
        }

        if (Lang.isNotEmpty(remoteJudgeDTO.getCookies())) {
            httpClient.cookies(remoteJudgeDTO.getCookies());
        } else {
            // 登录
        }
        Response response = httpClient.get(HOST + "/problem/" + problemId());
        Validator.validateTrue(response.isOK(), "login failed, status:" + response.getStatus());

        String captcha = null;
        int captchaTimes = 0;
        while (captchaTimes < 3) {
            NutMap params = NutMap.NEW()
                    .setv("enableO2", 0)
                    .setv("lang", 0)
                    .setv("code", remoteJudgeDTO.getUserCode());
            if (Strings.isNotBlank(captcha)) {
                captchaTimes++; // 验证码尝试次数
                params.setv("captcha", captcha);
            }

            // 提交请求
            Request request = Request.post(HOST + "/fe/api/problem/submit/" + problemId(),
                            Header.create()
                                    .set("Content-Type", "application/json")
                                    .set("Referer", HOST + "/problem/" + problemId()))
                    .setData(Json.toJson(params));
            response = httpClient.sendRequest(request);

            String body = UnicodeUtil.toString(response.getContent());
            if (response.isOK()) {
                // 提交成功
                log.info("submit success : {}", body);
                remoteJudgeDTO.setSubmitId(Json.fromJson(NutMap.class, body).getLong("rid"));
                return;
            }

            if (response.getStatus() == 403 && body.contains("验证码")) {
                // 等待验证码，尝试3次
                captcha = null;
                int times = 0;
                while (times < 3) {
                    times++;
                    Response rsp = httpClient.get(HOST + "/api/verify/captcha?_t=" + System.currentTimeMillis());
                    if (rsp.isOK()) {
                        captcha = parseCaptcha(IoUtil.readBytes(rsp.getStream()));
                        break;
                    }
                }

                if (Strings.isBlank(captcha)) {
                    log.error("parse captcha failed, judgeId:{}", remoteJudgeDTO.getJudgeId());
                    return;
                } else {
                    // 验证码解析成功，继续提交
                    continue;
                }
            }

            // 其他错误
            log.error("submit failed, status:{}, body:{}", response.getStatus(), body);

            if (body.startsWith("{")) {
                NutMap data = Json.fromJson(NutMap.class, body);
                throw new RuntimeException(data.getString("errorMessage"));
            }

            return;
        }   // end white
    }

    @Override
    public RemoteJudgeRes result() {
        RemoteJudgeDTO remoteJudgeDTO = getRemoteJudgeDTO();
        Long submitId = remoteJudgeDTO.getSubmitId();
        if (submitId == null) {
            return null;
        }

        int retry = 0;
        boolean isJudging = false;
        while (retry < 3 || isJudging) {
            Response response = httpClient.get(HOST + "/record/" + submitId);
            if (!response.isOK()) {
                retry++;
                Lang.quiteSleep(500);
                continue;
            }

            String body = response.getContent();
            body = StrUtil.subBetween(body, "decodeURIComponent(\"", "\"");
            body = URLDecoder.decode(body, CharsetUtil.CHARSET_GBK);
            NutMap data = Json.fromJson(NutMap.class, body);
            if (data.getInt("code") == 200) {
                data = data.getAs("currentData", NutMap.class);
                NutMap record = data.getAs("record", NutMap.class);
                NutMap detail = record.getAs("detail", NutMap.class);

                int status = record.getInt("status");
                if (status == 0 || status == 1) {
                    // 正在判题，1.5秒后重试
                    Lang.quiteSleep(1500);
                    isJudging = true;
                    continue;
                }

                RemoteJudgeRes remoteJudgeRes = RemoteJudgeRes.builder().build();
                switch (status) {
                    case 2:     // 编译错误
                        NutMap compileResult = detail.getAs("compileResult", NutMap.class);
                        remoteJudgeRes.setStatus(Constants.Judge.STATUS_COMPILE_ERROR.getStatus());
                        remoteJudgeRes.setErrorInfo(compileResult.getString("message"));
                        return remoteJudgeRes;
                    case 3:
                    case 4:
                    case 5:
                    case 6:     // WA
                    case 7:
                    case 11:
                    case 12:    // AC
                    case 14:    // PA 部分接受
                        remoteJudgeRes.setStatus(parseLuoguStatus(status));
                        remoteJudgeRes.setTime(record.getInt("time"));
                        remoteJudgeRes.setMemory(record.getInt("memory"));
                        NutMap judgeResult = detail.getAs("judgeResult", NutMap.class);
                        List<NutMap> subtasks = judgeResult.getAsList("subtasks", NutMap.class);
                        if (Lang.isNotEmpty(subtasks)) {
                            NutMap subtask = subtasks.get(0);
                            remoteJudgeRes.setJudgeCaseList(parseTestCases(subtask.get("testCases"), remoteJudgeDTO));
                            if (remoteJudgeRes.getJudgeCaseList().stream()
                                    .allMatch(it -> Constants.Judge.STATUS_WRONG_ANSWER.getStatus().equals(it.getStatus()))) {
                                // 全部未通过，改为WA
                                remoteJudgeRes.setStatus(Constants.Judge.STATUS_WRONG_ANSWER.getStatus());
                            }
                        }
                        return remoteJudgeRes;
                    default:
                        break;
                }
            }
        }

        return null;
    }

    @Override
    public void login() {

    }

    public RemoteOjLoginDTO getLoginCaptcha() {
        Response response = httpClient.get(HOST + "/auth/login");
        Validator.validateTrue(response.isOK(), "get login page failed");

        // 图形验证码
        response = httpClient.get("https://www.luogu.com.cn/lg4/captcha?_t=" + System.currentTimeMillis() + "." + RandomUtil.randomNumbers(4));
        Validator.validateTrue(response.isOK(), "get login captcha failed");

        // 设置图形验证码
        RemoteOjLoginDTO loginDTO = new RemoteOjLoginDTO();
        loginDTO.setCaptchaImage(IoUtil.readBytes(response.getStream()));
        loginDTO.setCookies(httpClient.cookies());
        loginDTO.addHeader("X-CSRF-Token", csrfTokenInterceptor.csrfToken);
        return loginDTO;
    }

    @Override
    public void loginWithCaptcha(RemoteOjLoginDTO loginDTO) {
        httpClient.cookies(loginDTO.getCookies());
        csrfTokenInterceptor.csrfToken = loginDTO.getHeaders().get("X-CSRF-Token");

        NutMap params = NutMap.NEW()
                .addv("username", loginDTO.getUsername())
                .addv("password", loginDTO.getPassword())
                .addv("captcha", loginDTO.getCaptchaCode());
        Response response = httpClient.postJson(HOST + "/do-auth/password", params);
        if (!response.isOK()) {
            String body = UnicodeUtil.toString(response.getContent());
            NutMap result = Json.fromJson(NutMap.class, body);
            throw new ServiceException(result.getString("errorMessage"));
        }

        // 登录成功，保存Cookie
        loginDTO.setCookies(httpClient.cookies());
    }

    @Override
    public String getLanguage(String language) {
        return null;
    }

    String userId() {
        RemoteJudgeDTO remoteJudgeDTO = getRemoteJudgeDTO();
        if (remoteJudgeDTO != null) {
            return remoteJudgeDTO.getUsername();
        }

        return null;
    }

    String clientId() {
        RemoteJudgeDTO remoteJudgeDTO = getRemoteJudgeDTO();
        if (remoteJudgeDTO != null) {
            return remoteJudgeDTO.getPassword();
        }

        return null;
    }

    String problemId() {
        RemoteJudgeDTO remoteJudgeDTO = getRemoteJudgeDTO();
        if (remoteJudgeDTO != null) {
            return remoteJudgeDTO.getCompleteProblemId();
        }

        return null;
    }

    static String getCookies(Map<String, String> map) {
        Set<String> keySet = map.keySet();
        StringBuilder sb = new StringBuilder();
        for (String key : keySet) {
            sb.append(key).append("=").append(map.get(key)).append("; ");
        }
        return sb.toString();
    }

    int parseLuoguStatus(int status) {
        Constants.Judge judgeState = LUOGU_STATUS_MAP.get(status);
        if (judgeState == null) {
            return Constants.Judge.STATUS_SUBMITTED_UNKNOWN_RESULT.getStatus();
        }

        return judgeState.getStatus();
    }

    List<JudgeCase> parseTestCases(Object obj, RemoteJudgeDTO remoteJudgeDTO) {
        List<NutMap> caseList = new ArrayList<>();
        if (obj instanceof NutMap) {
            NutMap map = (NutMap) obj;
            map.keySet().forEach(key -> {
                caseList.add(map.getAs(key, NutMap.class));
            });
        } else if (obj instanceof List) {
            ((List<?>) obj).forEach(item -> caseList.add((NutMap) item));
        }

        return caseList.stream().map(caseMap -> {
            JudgeCase judgeCase = new JudgeCase();
            judgeCase.setSubmitId(remoteJudgeDTO.getJudgeId());
            judgeCase.setPid(remoteJudgeDTO.getPid());
            judgeCase.setUid(remoteJudgeDTO.getUid());
            judgeCase.setCaseId(caseMap.getLong("id"));
            judgeCase.setSeq(caseMap.getInt("id"));
            judgeCase.setStatus(parseLuoguStatus(caseMap.getInt("status")));
            judgeCase.setTime(caseMap.getInt("time"));
            judgeCase.setMemory(caseMap.getInt("memory"));
            judgeCase.setScore(caseMap.getInt("score"));
            return judgeCase;
        }).collect(Collectors.toList());
    }


    /**
     * 通过接口解析图片验证码
     */
    private String parseCaptcha(byte[] bytes) {
        String image = Base64.encode(bytes);
        NutMap params = NutMap.NEW().addv("image", image)
                .addv("token", "Q9A93Vqnc-LXA0IziBCNQ_Toe4S9mfkSs83ib128Sy4")
                .addv("type", "10103");
        Header header = Header.create();
        header.addv("Content-Type", "application/json");

        Response response = Http.post3("http://api.jfbym.com/api/YmServer/customApi", Json.toJson(params),
                Header.create().set("Content-Type", "application/json"), 12000);
        if (!response.isOK()) {
            return null;
        }

        String content = response.getContent();
        NutMap result = Json.fromJson(NutMap.class, content);
        if (result.getInt("code") == 10000) {
            NutMap data = result.getAs("data", NutMap.class);
            String code = data.getString("data");
            log.info("验证码识别 : {}", code);
            return code;
        }

        log.error("parseCaptcha failed, status:{}, body:{}", response.getStatus(), content);
        return null;
    }

    static class CsrfTokenInterceptor implements HttpReqRespInterceptor {
        @Getter
        private String csrfToken;

        @Override
        public void beforeConnect(Request request) {
        }

        @Override
        public void afterConnect(Request request, HttpURLConnection conn) {
            if (csrfToken != null) {
                request.header("X-CSRF-Token", csrfToken);
            }
        }

        @Override
        public void afterResponse(Request request, HttpURLConnection conn, Response response) {
            String type = response.getHeader().get("Content-Type");
            if (Strings.isNotBlank(type) && type.contains("html")) {
                String body = response.getContent();
                if (body.contains("<meta name=\"csrf-token\" content=")) {
                    csrfToken = StrUtil.subBetween(body, "<meta name=\"csrf-token\" content=\"", "\">");
                }
            }
        }
    }
}
